/*
 * Copyright (c) 2021 Akshay Jadhav <jadhavakshay0701@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.ichi2.anki

import android.content.ClipDescription
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.annotation.CheckResult
import com.ichi2.anki.multimediacard.fields.ImageField
import com.ichi2.anki.multimediacard.fields.MediaClipField
import com.ichi2.compat.CompatHelper
import com.ichi2.libanki.exception.EmptyMediaException
import com.ichi2.utils.ClipboardUtil
import com.ichi2.utils.ContentResolverUtil.getFileName
import com.ichi2.utils.FileNameAndExtension
import timber.log.Timber
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.lang.IllegalStateException

/**
 * RegisterMediaForWebView is used for registering media in temp path,
 * this class is required in summer note class for paste image event and in visual editor activity for importing media,
 * (extracted code to avoid duplication of code).
 */
class MediaRegistration(
    private val context: Context,
) {
    // Use the same HTML if the same image is pasted multiple times.
    private val pastedMediaCache = HashMap<String, String?>()

    /**
     * Loads media into the collection.media directory and returns a HTML reference
     * @param uri The uri of the image to load
     * @return HTML referring to the loaded image
     */
    @Throws(IOException::class)
    fun loadMediaIntoCollection(
        uri: Uri,
        description: ClipDescription,
    ): String? {
        val filename = getFileName(context.contentResolver, uri)
        val fd = openInputStreamWithURI(uri)
        val (fileName, fileExtensionWithDot) =
            FileNameAndExtension
                .fromString(filename)
                ?.renameForCreateTempFile()
                ?: throw IllegalStateException("Unable to determine valid filename")
        var clipCopy: File
        var bytesWritten: Long
        val isImage = ClipboardUtil.hasImage(description)
        val isVideo = ClipboardUtil.hasVideo(description)

        openInputStreamWithURI(uri).use { copyFd ->
            // no conversion to jpg in cases of gif and jpg and if png image with alpha channel
            if (shouldConvertToJPG(fileExtensionWithDot, copyFd, isImage)) {
                clipCopy = File.createTempFile(fileName, ".jpg")
                bytesWritten = CompatHelper.compat.copyFile(fd, clipCopy.absolutePath)
                // return null if jpg conversion false.
                if (!convertToJPG(clipCopy)) {
                    return null
                }
            } else {
                clipCopy = File.createTempFile(fileName, fileExtensionWithDot)
                bytesWritten = CompatHelper.compat.copyFile(fd, clipCopy.absolutePath)
            }
        }
        val tempFilePath = clipCopy.absolutePath
        // register media for webView
        if (!registerMediaForWebView(tempFilePath)) {
            return null
        }
        Timber.d("File was %d bytes", bytesWritten)
        if (bytesWritten > MEDIA_MAX_SIZE) {
            Timber.w("File was too large: %d bytes", bytesWritten)
            val message =
                if (isImage) {
                    context.getString(R.string.note_editor_image_too_large)
                } else if (isVideo) {
                    context.getString(R.string.note_editor_video_too_large)
                } else {
                    context.getString(R.string.note_editor_audio_too_large)
                }
            showThemedToast(context, message, false)
            File(tempFilePath).delete()
            return null
        }
        val field =
            if (isImage) {
                ImageField()
            } else {
                MediaClipField()
            }
        field.hasTemporaryMedia = true
        field.mediaPath = tempFilePath
        return field.formattedValue
    }

    @Throws(FileNotFoundException::class)
    private fun openInputStreamWithURI(uri: Uri): InputStream = context.contentResolver.openInputStream(uri)!!

    private fun convertToJPG(file: File): Boolean {
        val bm = BitmapFactory.decodeFile(file.absolutePath)
        try {
            FileOutputStream(file.absolutePath).use { outStream ->
                bm.compress(Bitmap.CompressFormat.JPEG, 100, outStream)
                outStream.flush()
            }
        } catch (e: IOException) {
            Timber.w("MediaRegistration : Unable to convert file to png format")
            CrashReportService.sendExceptionReport(e, "Unable to convert file to png format")
            showThemedToast(context, context.resources.getString(R.string.multimedia_editor_png_paste_error, e.message), true)
            return false
        }
        return true // successful conversion to jpg.
    }

    private fun shouldConvertToJPG(
        fileNameExtension: String,
        fileStream: InputStream,
        isImage: Boolean,
    ): Boolean {
        if (!isImage) {
            return false
        }
        if (".svg" == fileNameExtension) {
            return false
        }
        if (".jpg" == fileNameExtension) {
            return false // we are already a jpg, no conversion
        }
        if (".gif" == fileNameExtension) {
            return false // gifs may have animation, conversion would ruin them
        }
        if (".png" == fileNameExtension && doesInputStreamContainTransparency(fileStream)) {
            return false // pngs with transparency would be ruined by conversion
        }
        return true
    }

    fun onPaste(
        uri: Uri,
        description: ClipDescription,
    ): String? =
        try {
            // check if cache already holds registered file or not
            if (!pastedMediaCache.containsKey(uri.toString())) {
                pastedMediaCache[uri.toString()] = loadMediaIntoCollection(uri, description)
            }
            pastedMediaCache[uri.toString()]
        } catch (ex: NullPointerException) {
            // Tested under FB Messenger and GMail, both apps do nothing if this occurs.
            // This typically works if the user copies again - don't know the exact cause

            //  java.lang.SecurityException: Permission Denial: opening provider
            //  org.chromium.chrome.browser.util.ChromeFileProvider from ProcessRecord{80125c 11262:com.ichi2.anki/u0a455}
            //  (pid=11262, uid=10455) that is not exported from UID 10057
            Timber.w(ex, "Failed to paste media")
            null
        } catch (ex: SecurityException) {
            Timber.w(ex, "Failed to paste media")
            null
        } catch (e: Exception) {
            // NOTE: This is happy path coding which works on Android 9.
            CrashReportService.sendExceptionReport("File is invalid issue:8880", "RegisterMediaForWebView:onImagePaste URI of file:$uri")
            Timber.w(e, "Failed to paste media")
            showThemedToast(context, context.getString(R.string.multimedia_editor_something_wrong), false)
            null
        }

    @CheckResult
    fun registerMediaForWebView(mediaPath: String?): Boolean {
        if (mediaPath == null) {
            // Nothing to register - continue with execution.
            return true
        }
        Timber.i("Adding media to collection: %s", mediaPath)
        val f = File(mediaPath)
        return try {
            CollectionManager.getColUnsafe().media.addFile(f)
            true
        } catch (e: IOException) {
            Timber.w(e, "Failed to add file")
            false
        } catch (e: EmptyMediaException) {
            Timber.w(e, "Failed to add file")
            false
        }
    }

    companion object {
        private const val MEDIA_MAX_SIZE = 5 * 1000 * 1000
        private const val COLOR_GREY = 0
        private const val COLOR_TRUE = 2
        private const val COLOR_INDEX = 3
        private const val COLOR_GREY_ALPHA = 4
        private const val COLOR_TRUE_ALPHA = 6

        /**
         * given an inputStream of a file,
         * returns true if found that it has transparency (in its header)
         * code: https://stackoverflow.com/a/31311718/14148406
         */
        private fun doesInputStreamContainTransparency(inputStream: InputStream): Boolean {
            try {
                // skip: png signature,header chunk declaration,width,height,bitDepth :
                inputStream.skip((12 + 4 + 4 + 4 + 1).toLong())
                when (inputStream.read()) {
                    COLOR_GREY_ALPHA, COLOR_TRUE_ALPHA -> return true
                    COLOR_INDEX, COLOR_GREY, COLOR_TRUE -> return false
                }
                return true
            } catch (e: Exception) {
                Timber.w(e, "Failed to check transparency of inputStream")
            }
            return false
        }
    }
}
